package mirrormonkey.util.annotations.parsing;

import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;

import mirrormonkey.util.annotations.control.IRClass;

/**
 * Collects annotations from <tt>AnnotatedElements</tt>. Which annotation types
 * are collected can be configured.
 * 
 * @author Philipp Christian Loewner
 * 
 */
public class AnnotationIR {

	/**
	 * Keep track of the parser because it supplies presets.
	 */
	public final AnnotationParser parser;

	/**
	 * Collect these annotation types.
	 */
	public final Collection<Class<? extends Annotation>> collectTypes;

	/**
	 * Annotations for each type from <tt>collectTypes</tt> are stored here
	 */
	public final Map<Class<?>, Annotation> collectedAnnotations;

	/**
	 * Creates a new <tt>AnnotationIR</tt> that will look for presets in the
	 * specified <tt>AnnotationParser</tt>. After creating the
	 * <tt>AnnotationIR</tt>, the user still has to specify which annotation
	 * types it should collect.
	 * 
	 * @param parser
	 *            the <tt>AnnotationParser</tt> to get presets from
	 * @param element
	 *            the class or member that this <tt>AnnotationIR</tt> is created
	 *            for
	 */
	@SuppressWarnings("unused")
	public AnnotationIR(AnnotationParser parser, AnnotatedElement element) {
		this.parser = parser;
		this.collectTypes = new HashSet<Class<? extends Annotation>>();
		collectedAnnotations = new HashMap<Class<?>, Annotation>();
	}

	/**
	 * Creates a new <tt>AnnotationIR</tt> with parser, collected types and
	 * collected annotations from a previous one.
	 * 
	 * @param previous
	 *            <tt>AnnotationIR</tt> that is to be replaced by this one
	 * @param element
	 *            the class or member that this <tt>AnnotationIR</tt> is created
	 *            for
	 */
	@SuppressWarnings("unused")
	public AnnotationIR(AnnotationIR previous, AnnotatedElement element) {
		parser = previous.parser;
		collectTypes = previous.collectTypes;
		collectedAnnotations = previous.collectedAnnotations;
	}

	/**
	 * Uses an IRClass to determine which annotation classes to start or stop
	 * collecting or to drop entirely.
	 * 
	 * @param irClass
	 *            control annotation that was discovered from the element that
	 *            this <tt>AnnotationIR</tt> keeps track of
	 */
	@SuppressWarnings("unchecked")
	public void parseIRClass(IRClass irClass) {
		for (Class<?> c : irClass.stopCollect()) {
			removeCollectType(c);
		}
		for (Class<?> c : irClass.removeCompletely()) {
			removeCollectedAnnotation(c);
		}
		for (Class<?> c : irClass.startCollect()) {
			addCollectType((Class<? extends Annotation>) c);
		}
	}

	/**
	 * When encountering an annotation with type <tt>type</tt>, it will be
	 * collected.
	 * 
	 * @param type
	 *            type of annotation to start collecting
	 */
	public void addCollectType(Class<? extends Annotation> type) {
		collectTypes.add(type);
	}

	/**
	 * When encountering an annotation with type <tt>type</tt>, it will no
	 * longer be collected.
	 * 
	 * @param type
	 *            type of annotations to stop collecting. Will not remove
	 *            annotations of that type that were collected so far.
	 */
	public void removeCollectType(Class<?> type) {
		collectTypes.remove(type);
	}

	/**
	 * Removes the previously collected annotation with type <tt>type</tt> (if
	 * any).
	 * 
	 * @param type
	 *            type of annotations to remove. This will not stop this
	 *            <tt>AnnotationIR</tt> to collect future annotations with that
	 *            type.
	 */
	public void removeCollectedAnnotation(Class<?> type) {
		collectedAnnotations.remove(type);
	}

	/**
	 * Collects all annotations from an <tt>AnnotatedElement</tt>. Additionally,
	 * the <tt>AnnotatedElement</tt> is checked for preset annotations and, if
	 * any are found, the annotations of their preset membersById are also
	 * collected.
	 * 
	 * This method only collects annotations of types that have been previously
	 * added to <tt>collectTypes</tt>.
	 * 
	 * @param element
	 *            the <tt>AnnotatedElement</tt> to collect annotations from
	 */

	public void parseAnnotatedElement(AnnotatedElement element) {
		for (Annotation i : element.getAnnotations()) {
			AnnotatedElement preset = parser.getPresetElement(i
					.annotationType());
			if (preset != null) {
				parseAnnotatedElement(preset);
			}
		}
		collectAnnotations(element);
	}

	/**
	 * Collects all annotations from an <tt>AnnotatedElement</tt> that are
	 * instances of types contained in <tt>collectTypes</tt>.
	 * 
	 * @param element
	 *            the <tt>AnnotatedElement</tt> to collect annotations from
	 */
	public void collectAnnotations(AnnotatedElement element) {
		for (Class<? extends Annotation> i : collectTypes) {
			Annotation a;
			if ((a = element.getAnnotation(i)) != null) {
				collectedAnnotations.put(i, a);
			}
		}
	}

	/**
	 * Returns a previously collected annotation of a specified type, if any
	 * annotation has been collected. If no annotation of that type has
	 * previously been collected, <tt>null</tt> is returned.
	 * 
	 * @param <T>
	 *            type of the annotation to return
	 * @param type
	 *            class of the collected annotation
	 * @return least recently collected annotation of type <tt>type</tt> or
	 *         <tt>null</tt> if no such annotation has been collected at all
	 */
	@SuppressWarnings("unchecked")
	public <T extends Annotation> T getCollectedAnnotation(Class<T> type) {
		return (T) collectedAnnotations.get(type);
	}

	/**
	 * Removes all previously collected annotations from this
	 * <tt>AnnotationIR</tt>. The types of annotations that will be collected in
	 * the future remains unchanged.
	 */
	public void clear() {
		collectedAnnotations.clear();
	}

}
